/****************************************************************************
 * Copyright (c) 2009 IBM Corporation and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Thomas Joiner - HttpClient 4 implementation
 *
 * SPDX-License-Identifier: EPL-2.0
 *****************************************************************************/
package org.eclipse.ecf.internal.provider.filetransfer.httpclient4;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.eclipse.ecf.core.util.Proxy;
import org.eclipse.ecf.core.util.ProxyAddress;
import org.eclipse.ecf.core.util.Trace;

public abstract class HttpClientProxyCredentialProvider implements CredentialsProvider {

	abstract protected Proxy getECFProxy();

	abstract protected Credentials getNTLMCredentials(Proxy proxy);

	private Map cachedCredentials;

	public HttpClientProxyCredentialProvider() {
		cachedCredentials = new ConcurrentHashMap<AuthScope, Credentials>();
	}

	public void setCredentials(AuthScope authscope, Credentials credentials) {
		if (authscope == null)
			throw new IllegalArgumentException("Authentication scope may not be null"); //$NON-NLS-1$
		this.cachedCredentials.put(authscope, credentials);
	}

	public Credentials getCredentials(AuthScope authscope) {
		Trace.entering(Activator.PLUGIN_ID, DebugOptions.METHODS_ENTERING, HttpClientProxyCredentialProvider.class, "getCredentials " + authscope); //$NON-NLS-1$

		// First check to see whether given authscope matches any authscope 
		// already cached.
		Credentials result = matchCredentials(this.cachedCredentials, authscope);
		// If we have a match, return credentials
		if (result != null)
			return result;
		// If we don't have a match, first get ECF proxy, if any
		Proxy proxy = getECFProxy();
		if (proxy == null)
			return null;

		// Make sure that authscope and proxy host and port match
		if (!matchAuthScopeAndProxy(authscope, proxy))
			return null;

		// Then match scheme, and get credentials from proxy (if it's scheme we know about)
		Credentials credentials = null;
		if ("ntlm".equalsIgnoreCase(authscope.getScheme())) { //$NON-NLS-1$
			credentials = getNTLMCredentials(proxy);
		} else if ("basic".equalsIgnoreCase(authscope.getScheme()) || //$NON-NLS-1$
				"digest".equalsIgnoreCase(authscope.getScheme())) { //$NON-NLS-1$
			final String proxyUsername = proxy.getUsername();
			final String proxyPassword = proxy.getPassword();
			// If credentials present for proxy then we're done
			if (proxyUsername != null) {
				credentials = new UsernamePasswordCredentials(proxyUsername, proxyPassword);
			}
		} else if ("negotiate".equalsIgnoreCase(authscope.getScheme())) { //$NON-NLS-1$
			Trace.trace(Activator.PLUGIN_ID, "SPNEGO is not supported, if you can contribute support, please do so."); //$NON-NLS-1$
		} else {
			Trace.trace(Activator.PLUGIN_ID, "Unrecognized authentication scheme."); //$NON-NLS-1$
		}

		// Put found credentials in cache for next time
		if (credentials != null)
			cachedCredentials.put(authscope, credentials);

		return credentials;
	}

	private boolean matchAuthScopeAndProxy(AuthScope authscope, Proxy proxy) {
		ProxyAddress proxyAddress = proxy.getAddress();
		return (authscope.getHost().equals(proxyAddress.getHostName()) && (authscope.getPort() == proxyAddress.getPort()));
	}

	private static Credentials matchCredentials(final Map<AuthScope, Credentials> map, final AuthScope authscope) {
		// see if we get a direct hit
		Credentials creds = map.get(authscope);
		if (creds == null) {
			// Nope.
			// Do a full scan
			int bestMatchFactor = -1;
			AuthScope bestMatch = null;
			for (AuthScope current : map.keySet()) {
				int factor = authscope.match(current);
				if (factor > bestMatchFactor) {
					bestMatchFactor = factor;
					bestMatch = current;
				}
			}
			if (bestMatch != null) {
				creds = map.get(bestMatch);
			}
		}
		return creds;
	}

	public void clear() {
		this.cachedCredentials.clear();
	}
}
